/*
 * libjingle
 * Copyright 2004--2011, Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "talk/app/webrtc/webrtcsession.h"

#include <string>
#include <vector>

#include "talk/base/common.h"
#include "talk/base/json.h"
#include "talk/base/scoped_ptr.h"
#include "talk/p2p/base/constants.h"
#include "talk/p2p/base/sessiondescription.h"
#include "talk/p2p/base/p2ptransport.h"
#include "talk/session/phone/channel.h"
#include "talk/session/phone/channelmanager.h"
#include "talk/session/phone/mediasessionclient.h"
#include "talk/session/phone/voicechannel.h"

namespace webrtc {

enum {
  MSG_CANDIDATE_TIMEOUT = 101,
};

static const int kAudioMonitorPollFrequency = 100;
static const int kMonitorPollFrequency = 1000;

// We allow 30 seconds to establish a connection; beyond that we consider
// it an error
static const int kCallSetupTimeout = 30 * 1000;
// A loss of connectivity is probably due to the Internet connection going
// down, and it might take a while to come back on wireless networks, so we
// use a longer timeout for that.
static const int kCallLostTimeout = 60 * 1000;

static const char kVideoStream[] = "video_rtp";
static const char kAudioStream[] = "rtp";

WebRtcSession::WebRtcSession(const std::string& id,
                             bool incoming,
                             cricket::PortAllocator* allocator,
                             cricket::ChannelManager* channelmgr,
                             talk_base::Thread* signaling_thread)
    : BaseSession(signaling_thread, channelmgr->worker_thread(),
                  allocator, id, "", !incoming),
      transport_(NULL),
      channel_manager_(channelmgr),
      transports_writable_(false),
      muted_(false),
      camera_muted_(false),
      setup_timeout_(kCallSetupTimeout),
      signaling_thread_(signaling_thread),
      incoming_(incoming),
      port_allocator_(allocator),
      desc_factory_(channel_manager_) {
}

WebRtcSession::~WebRtcSession() {
  RemoveAllStreams();
  // TODO: Do we still need Terminate?
  // if (state_ != STATE_RECEIVEDTERMINATE) {
  //   Terminate();
  // }
  if (transport_) {
    delete transport_;
    transport_ = NULL;
  }
}

bool WebRtcSession::Initiate() {
  if (signaling_thread_ == NULL)
    return false;

  transport_ = CreateTransport();

  if (transport_ == NULL)
    return false;

  transport_->set_allow_local_ips(true);

  // start transports
  transport_->SignalRequestSignaling.connect(
      this, &WebRtcSession::OnRequestSignaling);
  transport_->SignalCandidatesReady.connect(
      this, &WebRtcSession::OnCandidatesReady);
  transport_->SignalWritableState.connect(
      this, &WebRtcSession::OnWritableState);
  // Limit the amount of time that setting up a call may take.
  StartTransportTimeout(kCallSetupTimeout);
  return true;
}

cricket::Transport* WebRtcSession::CreateTransport() {
  ASSERT(signaling_thread()->IsCurrent());
  return new cricket::P2PTransport(
      talk_base::Thread::Current(),
      channel_manager_->worker_thread(), port_allocator());
}

bool WebRtcSession::CreateVoiceChannel(const std::string& stream_id) {
  // RTCP disabled
  cricket::VoiceChannel* voice_channel =
      channel_manager_->CreateVoiceChannel(this, stream_id, true);
  if (voice_channel == NULL) {
    LOG(LERROR) << "Unable to create voice channel.";
    return false;
  }
  StreamInfo* stream_info = new StreamInfo(stream_id);
  stream_info->channel = voice_channel;
  stream_info->video = false;
  streams_.push_back(stream_info);
  return true;
}

bool WebRtcSession::CreateVideoChannel(const std::string& stream_id) {
  // RTCP disabled
  cricket::VideoChannel* video_channel =
      channel_manager_->CreateVideoChannel(this, stream_id, true, NULL);
  if (video_channel == NULL) {
    LOG(LERROR) << "Unable to create video channel.";
    return false;
  }
  StreamInfo* stream_info = new StreamInfo(stream_id);
  stream_info->channel = video_channel;
  stream_info->video = true;
  streams_.push_back(stream_info);
  return true;
}

cricket::TransportChannel* WebRtcSession::CreateChannel(
    const std::string& content_name,
    const std::string& name) {
  if (!transport_) {
    return NULL;
  }
  std::string type;
  if (content_name.compare(kVideoStream) == 0) {
    type = cricket::NS_GINGLE_VIDEO;
  } else {
    type = cricket::NS_GINGLE_AUDIO;
  }
  cricket::TransportChannel* transport_channel =
      transport_->CreateChannel(name, type);
  ASSERT(transport_channel != NULL);
  return transport_channel;
}

cricket::TransportChannel* WebRtcSession::GetChannel(
    const std::string& content_name, const std::string& name) {
  if (!transport_)
    return NULL;

  return transport_->GetChannel(name);
}

void WebRtcSession::DestroyChannel(
    const std::string& content_name, const std::string& name) {
  if (!transport_)
    return;

  transport_->DestroyChannel(name);
}

void WebRtcSession::OnMessage(talk_base::Message* message) {
  switch (message->message_id) {
    case MSG_CANDIDATE_TIMEOUT:
      if (transport_->writable()) {
        // This should never happen: The timout triggered even
        // though a call was successfully set up.
        ASSERT(false);
      }
      SignalFailedCall();
      break;
    default:
      cricket::BaseSession::OnMessage(message);
      break;
  }
}

bool WebRtcSession::Connect() {
  if (streams_.empty()) {
    // nothing to initiate
    return false;
  }
  // lets connect all the transport channels created before for this session
  transport_->ConnectChannels();

  // create an offer now. This is to call SetState
  // Actual offer will be send when OnCandidatesReady callback received
  cricket::SessionDescription* offer = CreateOffer();
  set_local_description(offer);
  SetState((incoming()) ? STATE_SENTACCEPT : STATE_SENTINITIATE);

  // Enable all the channels
  EnableAllStreams();
  SetVideoCapture(true);
  return true;
}

bool WebRtcSession::SetVideoRenderer(const std::string& stream_id,
                                     cricket::VideoRenderer* renderer) {
  bool ret = false;
  StreamMap::iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    StreamInfo* stream_info = (*iter);
    if (stream_info->stream_id.compare(stream_id) == 0) {
      ASSERT(stream_info->channel != NULL);
      ASSERT(stream_info->video);
      cricket::VideoChannel* channel = static_cast<cricket::VideoChannel*>(
          stream_info->channel);
      ret = channel->SetRenderer(0, renderer);
      break;
    }
  }
  return ret;
}

bool WebRtcSession::SetVideoCapture(bool capture) {
  channel_manager_->SetVideoCapture(capture);
  return true;
}

bool WebRtcSession::RemoveStream(const std::string& stream_id) {
  bool ret = false;
  StreamMap::iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    StreamInfo* sinfo = (*iter);
    if (sinfo->stream_id.compare(stream_id) == 0) {
      if (!sinfo->video) {
        cricket::VoiceChannel* channel = static_cast<cricket::VoiceChannel*> (
            sinfo->channel);
        channel->Enable(false);
        // Note: If later the channel is used by multiple streams, then we
        // should not destroy the channel until all the streams are removed.
        channel_manager_->DestroyVoiceChannel(channel);
      } else {
        cricket::VideoChannel* channel = static_cast<cricket::VideoChannel*> (
            sinfo->channel);
        channel->Enable(false);
        // Note: If later the channel is used by multiple streams, then we
        // should not destroy the channel until all the streams are removed.
        channel_manager_->DestroyVideoChannel(channel);
      }
      // channel and transport will be deleted in
      // DestroyVoiceChannel/DestroyVideoChannel
      streams_.erase(iter);
      ret = true;
      break;
    }
  }
  if (!ret) {
    LOG(LERROR) << "No streams found for stream id " << stream_id;
    // TODO: trigger onError callback
  }
  return ret;
}

void WebRtcSession::EnableAllStreams() {
  StreamMap::const_iterator i;
  for (i = streams_.begin(); i != streams_.end(); ++i) {
    cricket::BaseChannel* channel = (*i)->channel;
    if (channel)
      channel->Enable(true);
  }
}

void WebRtcSession::RemoveAllStreams() {
  SetState(STATE_RECEIVEDTERMINATE);

  // signaling_thread_->Post(this, MSG_RTC_REMOVEALLSTREAMS);
  // First build a list of streams to remove and then remove them.
  // The reason we do this is that if we remove the streams inside the
  // loop, a stream might get removed while we're enumerating and the iterator
  // will become invalid (and we crash).
  // streams_ entry will be removed from ChannelManager callback method
  // DestroyChannel
  std::vector<std::string> streams_to_remove;
  StreamMap::iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter)
    streams_to_remove.push_back((*iter)->stream_id);

  for (std::vector<std::string>::iterator i = streams_to_remove.begin();
       i != streams_to_remove.end(); ++i) {
    RemoveStream(*i);
  }
}

bool WebRtcSession::HasStream(const std::string& stream_id) const {
  StreamMap::const_iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    StreamInfo* sinfo = (*iter);
    if (stream_id.compare(sinfo->stream_id) == 0) {
      return true;
    }
  }
  return false;
}

bool WebRtcSession::HasChannel(bool video) const {
  StreamMap::const_iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    StreamInfo* sinfo = (*iter);
    if (sinfo->video == video) {
      return true;
    }
  }
  return false;
}

bool WebRtcSession::HasAudioChannel() const {
  return HasChannel(false);
}

bool WebRtcSession::HasVideoChannel() const {
  return HasChannel(true);
}

void WebRtcSession::OnRequestSignaling(cricket::Transport* transport) {
  transport->OnSignalingReady();
}

void WebRtcSession::OnWritableState(cricket::Transport* transport) {
  ASSERT(transport == transport_);
  const bool transports_writable = transport_->writable();
  if (transports_writable) {
    if (transports_writable != transports_writable_) {
      signaling_thread_->Clear(this, MSG_CANDIDATE_TIMEOUT);
    } else {
      // At one point all channels were writable and we had full connectivity,
      // but then we lost it. Start the timeout again to kill the call if it
      // doesn't come back.
      StartTransportTimeout(kCallLostTimeout);
    }
    transports_writable_ = transports_writable;
  }
  NotifyTransportState();
  return;
}

void WebRtcSession::StartTransportTimeout(int timeout) {
  talk_base::Thread::Current()->PostDelayed(timeout, this,
                                            MSG_CANDIDATE_TIMEOUT,
                                            NULL);
}

void WebRtcSession::NotifyTransportState() {
}

bool WebRtcSession::OnInitiateMessage(
    cricket::SessionDescription* offer,
    const std::vector<cricket::Candidate>& candidates) {
  if (!offer) {
    LOG(LERROR) << "No SessionDescription from peer";
    return false;
  }

  // Get capabilities from offer before generating an answer to it.
  cricket::MediaSessionOptions options;
  if (GetFirstAudioContent(offer))
    options.has_audio = true;
  if (GetFirstVideoContent(offer))
    options.has_video = true;

  talk_base::scoped_ptr<cricket::SessionDescription> answer;
  answer.reset(CreateAnswer(offer, options));

  if (!answer.get()) {
    return false;
  }

  const cricket::ContentInfo* audio_content = GetFirstAudioContent(
      answer.get());
  const cricket::ContentInfo* video_content = GetFirstVideoContent(
      answer.get());

  if (!audio_content && !video_content) {
    return false;
  }

  bool ret = true;
  if (audio_content) {
    ret = !HasAudioChannel() &&
          CreateVoiceChannel(audio_content->name);
    if (!ret) {
      LOG(LERROR) << "Failed to create voice channel for "
                  << audio_content->name;
      return false;
    }
  }

  if (video_content) {
    ret = !HasVideoChannel() &&
          CreateVideoChannel(video_content->name);
    if (!ret) {
      LOG(LERROR) << "Failed to create video channel for "
                  << video_content->name;
      return false;
    }
  }
  // Provide remote candidates to the transport
  transport_->OnRemoteCandidates(candidates);

  set_remote_description(offer);
  SetState(STATE_RECEIVEDINITIATE);

  transport_->ConnectChannels();
  EnableAllStreams();

  set_local_description(answer.release());

  // AddStream called only once with Video label
  if (video_content) {
    SignalAddStream(video_content->name, true);
  } else {
    SignalAddStream(audio_content->name, false);
  }
  SetState(STATE_SENTACCEPT);
  return true;
}

bool WebRtcSession::OnRemoteDescription(
    cricket::SessionDescription* desc,
    const std::vector<cricket::Candidate>& candidates) {
  if (state() == STATE_SENTACCEPT ||
      state() == STATE_RECEIVEDACCEPT ||
      state() == STATE_INPROGRESS) {
    transport_->OnRemoteCandidates(candidates);
    return true;
  }
  // Session description is always accepted.
  set_remote_description(desc);
  SetState(STATE_RECEIVEDACCEPT);
  // Will trigger OnWritableState() if successful.
  transport_->OnRemoteCandidates(candidates);

  if (!incoming()) {
    // Trigger OnAddStream callback at the initiator
    const cricket::ContentInfo* video_content = GetFirstVideoContent(desc);
    if (video_content && !SendSignalAddStream(true)) {
      LOG(LERROR) << "Video stream unexpected in answer.";
      return false;
    } else {
      const cricket::ContentInfo* audio_content = GetFirstAudioContent(desc);
      if (audio_content && !SendSignalAddStream(false)) {
        LOG(LERROR) << "Audio stream unexpected in answer.";
        return false;
      }
    }
  }
  return true;
}

// Send the SignalAddStream with the stream_id based on the content type.
bool WebRtcSession::SendSignalAddStream(bool video) {
  StreamMap::const_iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    StreamInfo* sinfo = (*iter);
    if (sinfo->video == video) {
      SignalAddStream(sinfo->stream_id, video);
      return true;
    }
  }
  return false;
}

cricket::SessionDescription* WebRtcSession::CreateOffer() {
  cricket::MediaSessionOptions options;
  options.has_audio = false;  // disable default option
  StreamMap::const_iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    if ((*iter)->video) {
      options.has_video = true;
    } else {
      options.has_audio = true;
    }
  }
  return desc_factory_.CreateOffer(options);
}

cricket::SessionDescription* WebRtcSession::CreateAnswer(
    const cricket::SessionDescription* offer,
    const cricket::MediaSessionOptions& options) {
  return desc_factory_.CreateAnswer(offer, options);
}

void WebRtcSession::SetError(Error error) {
  BaseSession::SetError(error);
}

void WebRtcSession::OnCandidatesReady(
    cricket::Transport* transport,
    const std::vector<cricket::Candidate>& candidates) {
  std::vector<cricket::Candidate>::const_iterator iter;
  for (iter = candidates.begin(); iter != candidates.end(); ++iter) {
    local_candidates_.push_back(*iter);
  }
  SignalLocalDescription(local_description(), candidates);
}
} /* namespace webrtc */
